use std::{
    io::Read,
    path::{Path, PathBuf},
    process::{Child, Command},
    time::{Duration, Instant},
};

use clap::Parser;

#[derive(clap::Parser)]
struct CommandLine {
    last_status: i32,

    /// Count of current running jobs.
    #[clap(short, long, default_value = "0")]
    jobs: u32,

    /// Timeout of plugins.
    #[clap(short, long, default_value = "5")]
    plugin_timeout_ms: u32,

    /// Use ./plugins/* instead of ~/.local/share/jia-prompt/plugins/*
    #[clap(short, long, default_value = "false")]
    local_plugins: bool,
}

#[derive(Debug, Clone, Copy)]
enum Color {
    Reset = 0,
    Red = 31,
    Green = 32,
    Yellow = 33,
    Blue = 34,
    Magenta = 35,
    Cyan = 36,
}

#[derive(Clone)]
enum PluginStatus {
    Running,
    Error,
    Done { plugin_name: String, output: String },
}

fn get_plugin_status(plugin_name: &str, child: &mut Child) -> PluginStatus {
    let Ok(Some(status)) = child.try_wait() else {
        return PluginStatus::Running;
    };

    if !status.success() {
        eprintln!(
            "[jia-prompt] plugin report error, status: {:?}",
            status.code(),
        );
        return PluginStatus::Error;
    }

    let mut stdout = child.stdout.take().unwrap();

    let mut buf = String::new();
    let Ok(_) = stdout.read_to_string(&mut buf) else {
        eprintln!(
            "[jia-prompt] failed to read output of plugin, plugin: {}",
            plugin_name
        );
        return PluginStatus::Error;
    };

    PluginStatus::Done {
        output: buf.to_string(),
        plugin_name: plugin_name.to_owned(),
    }
}

fn start_plugins(cli: &CommandLine, plugin_paths: &[PathBuf]) -> Vec<(String, Child)> {
    let mut plugins = Vec::with_capacity(plugin_paths.len());

    for plugin_path in plugin_paths {
        let Some(plugin_name) = plugin_path.file_name() else {
            continue;
        };

        let Some(plugin_name) = plugin_name.to_str() else {
            eprintln!(
                "[jia-prompt] invalid plugin name, plugin name: {}",
                plugin_name.display(),
            );
            continue;
        };

        let Ok(child) = Command::new(plugin_path)
            .stdout(std::process::Stdio::piped())
            .stdin(std::process::Stdio::null())
            .env("JIA_PROMPT_jobs", cli.jobs.to_string())
            .env("JIA_PROMPT_status", cli.last_status.to_string())
            .spawn()
        else {
            eprintln!("[jia-prompt] failed to start plugin, name: {}", plugin_name,);
            continue;
        };

        eprintln!("[jia-prompt] start plugin, name: {}", plugin_name);

        plugins.push((plugin_name.to_owned(), child));
    }

    plugins
}

fn check_plugins(outputs: &mut Vec<PluginStatus>, plugins: &mut Vec<(String, Child)>) -> bool {
    let mut any_running = false;

    for (i, (name, each)) in plugins.iter_mut().enumerate() {
        if !matches!(outputs[i], PluginStatus::Running) {
            continue;
        }

        any_running = true;
        outputs[i] = get_plugin_status(&name, each);
    }

    !any_running
}

fn main() {
    let start = Instant::now();
    runtime();
    eprintln!("[jia-prompt] runtime exit, cost: {:?}", start.elapsed());
}

fn load_plugin_paths(local_plugins: bool) -> Vec<PathBuf> {
    let plugins_dir = if local_plugins {
        Path::new("./plugins").to_path_buf()
    } else {
        let Some(data_dir) = dirs::data_dir() else {
            return vec![];
        };
        data_dir.join("jia-prompt").join("plugins")
    };

    let Ok(read_dir) = std::fs::read_dir(&plugins_dir) else {
        return vec![];
    };

    let mut paths = vec![];

    for each in read_dir {
        let Ok(each) = each else {
            continue;
        };

        paths.push(each.path());
    }

    paths
}

fn parse_and_print_views(plugin_name: &str, text: &str) -> Result<(), ()> {
    for line in text.split('\n') {
        let line = line.trim();
        let (key, value) = line.split_once(':').unwrap_or(("text", line));
        let key = key.trim();
        let value = value.trim();

        match key {
            "snippet" => match value {
                "open" => {
                    print!(" (");
                    print_color(Color::Cyan);
                }
                "close" => {
                    print_color(Color::Reset);
                    print!(")");
                }
                _ => {
                    eprintln!(
                        "[jia-prompt] invalid snippet, plugin: {}, value: {}",
                        plugin_name, value
                    );
                    return Err(());
                }
            },
            "text" => {
                print!("{}", value);
            }
            "color" => {
                let color = parse_color(value)?;

                print_color(color);
            }
            "newline" => {
                let n: usize = value.parse().map_err(|err| {
                    eprintln!(
                        "[jia-prompt] failed to parse value to number, plugin: {plugin_name}, key: {key}, value: {value}, error: {err}",
                    )
                })?;

                for _ in 0..n {
                    println!();
                }
            }
            "space" => {
                let n: usize = value.parse().map_err(|err| {
                    eprintln!(
                        "[jia-prompt] failed to parse value to number, plugin: {plugin_name}, key: {key}, value: {value}, error: {err}",
                    )
                })?;

                for _ in 0..n {
                    print!(" ");
                }
            }
            _ => {
                eprintln!(
                    "[jia-prompt] invalid option, plugin: {plugin_name}, key: {key}, value: {value}"
                );
                return Err(());
            }
        }
    }

    Ok(())
}

fn print_color(color: Color) {
    const COLOR_START: &str = "\x1b[";
    const COLOR_END: &str = "m";

    print!("{}{}{}", COLOR_START, color as u32, COLOR_END);
}

fn runtime() {
    let cli = CommandLine::parse();

    let mut paths = load_plugin_paths(cli.local_plugins);
    paths.sort();

    let mut plugins = start_plugins(&cli, &paths);
    let mut status = vec![PluginStatus::Running; plugins.len()];

    if !check_plugins(&mut status, &mut plugins) {
        std::thread::sleep(Duration::from_millis(cli.plugin_timeout_ms as u64));
        check_plugins(&mut status, &mut plugins);
    }

    for each in &status {
        let PluginStatus::Done {
            output,
            plugin_name,
        } = each
        else {
            eprintln!("[jia-prompt] plugin has no output");
            continue;
        };

        eprintln!("[jia-prompt] show plugin, plugin: {}", plugin_name);

        let _ = parse_and_print_views(&plugin_name, output);
    }
}

fn parse_color(value: &str) -> Result<Color, ()> {
    Ok(match value.trim() {
        "reset" => Color::Reset,

        "red" => Color::Red,
        "green" => Color::Green,
        "yellow" => Color::Yellow,
        "blue" => Color::Blue,
        "magenta" => Color::Magenta,
        "cyan" => Color::Cyan,

        _ => {
            eprintln!("[jia-prompt] failed to parse color: {}", value);
            return Err(());
        }
    })
}
